package cn.tedu.eshihui.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

// ======================================
// AOP：面向切面的编程
// 注意：AOP是一门的技术，起始与Spring无关，其技术名称是AspectJ，而Spring很好的支持了AOP，基于Spring使用AOP是主流的做法
// AOP主要解决项目中的特定问题，例如：日志、事务管理、安全等
// 在项目中，数据的处理流程大概是：
// - 用户登录：请求 ---> Controller ---> Service ---> Mapper
// - 新增相册：请求 ---> Controller ---> Service ---> Mapper
// - 删除类别：请求 ---> Controller ---> Service ---> Mapper
// - 属性列表：请求 ---> Controller ---> Service ---> Mapper
// 如果存在某个业务需求，是用户登录、新增相册、删除类别等都需要在Service实现的，例如统计各业务的执行耗时
// 以上需求使用AOP来实现是最合适的
// ======================================
// 在Spring Boot中，实现AOP需要添加依赖：spring-boot-starter-aop
// ======================================
@Slf4j
@Aspect
@Component
public class TimerAspect {

    // AOP的核心概念
    // 连接点（JoinPoint）：数据处理过程中的某个节点，可能是调用了某个方法，或抛出异常
    // 切入点（PointCut）：选择1个或多个连接点的表达式
    // ----------------------------------------------------------
    // 通知（Advice）注解
    // @Around：环绕，在execution表达式匹配的方法之前和之后都执行特定的代码
    // @Before：在……之前
    // @After：在……之后
    // @AfterReturning：仅在返回之后执行
    // @AfterThrowing：仅在抛出异常之后执行
    // 以上各通知的执行类似于：
    // @Around开始
    // try {
    //    @Before
    //    执行execution匹配的方法
    //    @AfterReturning
    // } catch (Throwable e) {
    //    @AfterThrowing
    // } finally {
    //    @After
    // }
    // @Around结束
    // ----------------------------------------------------------
    // 在切入点表达式（execution表达式）中，允许使用2种通配符，分别是星号和连续的2个小数点
    // 星号：匹配任意内容，只匹配1次
    // 连续的2个小数点：匹配任意内容，可以匹配n次（n最小为0），只能用于包名和方法的参数列表
    // 另外，在“返回值类型”的左侧，还可以定义注解，此处是可选的，注意：类型需要写全限定名
    // ----------------------------------------------------------
    //       ↓↓↓↓↓↓↓↓↓ 固定
    //                 ↓ 星号：任意返回值类型，包括void
    //                   ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 根包
    //                                                  ↓ 星号：类名
    //                                                    ↓ 星号：方法名
    //                                                      ↓↓ 连续的2个小数点：任意参数
    @Around("execution(* cn.tedu.eshihui.service.*.*(..))")
    public Object timer(ProceedingJoinPoint pjp) throws Throwable {
        log.debug("开始执行业务方法……");
        long start = System.currentTimeMillis();

        String typeName = pjp.getTarget().getClass().getName();// 类型
        String methodName = pjp.getSignature().getName(); // 方法名称
        Object[] args = pjp.getArgs(); // 方法的参数列表
        log.debug("业务类：{}", typeName);
        log.debug("业务方法：{}", methodName);
        log.debug("业务方法的参数列表：{}", args);

        // 调用pjp.proceed()表示：调用execution表达式匹配的方法，并获取返回值
        // 注意-1：必须获取pjp.proceed()方法的返回值，作为当前切面方法的返回值，否则，相当于调用了匹配到的方法，但是没有获取返回值
        // 注意-2：调用的pjp.proceed()方法会抛出异常，此时必须抛出，不可以try...catch处理，除非，在catch中再次抛出异常
        Object result = pjp.proceed();

        long end = System.currentTimeMillis();
        log.debug("业务方法执行结束，耗时：" + (end - start));
        return result; // 必须返回调用pjp.proceed()得到的结果
    }

}
